package net.oschina.bilbodai.common.beanutil.bean;

import net.oschina.bilbodai.common.beanutil.bean.code.IPropertyCodeApplier;
import net.oschina.bilbodai.common.beanutil.exception.PropertyDumpException;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;

import java.util.List;

import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACONST_NULL;
import static org.objectweb.asm.Opcodes.ALOAD;
import static org.objectweb.asm.Opcodes.ARETURN;
import static org.objectweb.asm.Opcodes.ASTORE;
import static org.objectweb.asm.Opcodes.ATHROW;
import static org.objectweb.asm.Opcodes.CHECKCAST;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.GOTO;
import static org.objectweb.asm.Opcodes.IFNONNULL;
import static org.objectweb.asm.Opcodes.INVOKEINTERFACE;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.NEW;

/**
 * <pre>
 *     IBeanAccessor:
 *          T propertyDump(S src, T target)
 *          Locals:    0            1            2            3             4
 *                  this     (obj)src   (obj)target     srcCast     targetCast
 * </pre>
 *
 * @author daimao  Date: 16/4/21 Time: 下午1:22
 * @version $Id$
 */
public class BeanInfo {

    public static final int LOCAL_INDEX_THIS = 0;
    public static final int LOCAL_INDEX_OBJ_SRC = 1;
    public static final int LOCAL_INDEX_OBJ_TARGET = 2;
    public static final int LOCAL_INDEX_CAST_SRC = 3;
    public static final int LOCAL_INDEX_CAST_TARGET = 4;

    public static final int LOCAL_INDEX_COLLECT_CREATE_ACCESSOR = 5;
    public static final int LOCAL_INDEX_COLLECT_ITERATOR = 6;
    public static final int LOCAL_INDEX_COLLECT_ITERATOR_NEXT = 7;
    public static final int LOCAL_INDEX_MAP_ENTRY = 8;
    public static final int LOCAL_INDEX_EXCEPTION = LOCAL_INDEX_CAST_SRC; //即将抛出全局异常,随便借用一个local放入异常实例即可

    private final String srcInternalName;
    private final String targetInternalName;
    private final String dumpMethodName = BeanDumpUtil.PROPERTY_DUMP_METHOD.getName();
    private final String dumpMethodName1 = BeanDumpUtil.PROPERTY_DUMP_METHOD1.getName();
    private final String dumpMethodDesc = Type.getMethodDescriptor(BeanDumpUtil.PROPERTY_DUMP_METHOD);
    private final String dumpMethodDesc1 = Type.getMethodDescriptor(BeanDumpUtil.PROPERTY_DUMP_METHOD1);

    private final List<IPropertyCodeApplier> propertyCodeAppliers;

    public BeanInfo(String srcInternalName, String targetInternalName, List<IPropertyCodeApplier> propertyCodeAppliers) {
        this.srcInternalName = srcInternalName;
        this.targetInternalName = targetInternalName;
        this.propertyCodeAppliers = propertyCodeAppliers;
    }

    public void apply1(ClassVisitor cw) {
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, dumpMethodName1, dumpMethodDesc1, null, null);
        mv.visitCode();

        mv.visitVarInsn(ALOAD, 0);
        mv.visitVarInsn(ALOAD, 1);
        mv.visitInsn(ACONST_NULL);
        mv.visitMethodInsn(INVOKEINTERFACE, Type.getInternalName(IBeanAccessor.class), dumpMethodName, dumpMethodDesc, true);
        mv.visitInsn(ARETURN);
        //never mind the numbers, cause asm will calculate it for itself
        mv.visitMaxs(4, 9);
        mv.visitEnd();
    }

    public void apply(ClassVisitor cw) {
        apply0(cw);
        apply1(cw);
    }

    public void apply0(ClassVisitor cw) {
        MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, dumpMethodName, dumpMethodDesc, null, null);
        mv.visitCode();

        Label tryStartLabel = new Label();
        Label tryEndLabel = new Label();
        Label tryExHandleLabel = new Label();
        mv.visitTryCatchBlock(tryStartLabel, tryEndLabel, tryExHandleLabel, "java/lang/Exception");

        mv.visitLabel(tryStartLabel); //try{

        mv.visitVarInsn(ALOAD, LOCAL_INDEX_OBJ_SRC);
        mv.visitTypeInsn(CHECKCAST, srcInternalName);
        mv.visitVarInsn(ASTORE, LOCAL_INDEX_CAST_SRC);

        mv.visitVarInsn(ALOAD, LOCAL_INDEX_OBJ_TARGET);
        Label targetNullCheckLabelStart = new Label();
        mv.visitJumpInsn(IFNONNULL, targetNullCheckLabelStart); // if(target == null){
        mv.visitTypeInsn(NEW, targetInternalName);
        mv.visitInsn(DUP);
        mv.visitMethodInsn(INVOKESPECIAL, targetInternalName, "<init>", "()V", false);
        Label targetNullCheckLabelEnd = new Label();
        mv.visitJumpInsn(GOTO, targetNullCheckLabelEnd);

        mv.visitLabel(targetNullCheckLabelStart);

        mv.visitVarInsn(ALOAD, LOCAL_INDEX_OBJ_TARGET);
        mv.visitTypeInsn(CHECKCAST, targetInternalName);// cast target
        mv.visitLabel(targetNullCheckLabelEnd);
        mv.visitVarInsn(ASTORE, LOCAL_INDEX_CAST_TARGET);

        for (IPropertyCodeApplier propertyCodeApplier : propertyCodeAppliers) {
            propertyCodeApplier.apply(mv);
        }

        mv.visitVarInsn(ALOAD, LOCAL_INDEX_CAST_TARGET);

        mv.visitLabel(tryEndLabel); // } catch (
        mv.visitInsn(ARETURN);

        mv.visitLabel(tryExHandleLabel); //(Exception e) {throw new XX}
        // frame is not actually needed, cause asm will calculate it for itself
        //        mv.visitFrame(Opcodes.F_FULL, 3, new Object[] {Type.getInternalName(IBeanAccessor.class), "java/lang/Object", "java/lang/Object"}, 1, new Object[] {"java/lang/Exception"});
        mv.visitVarInsn(ASTORE, LOCAL_INDEX_EXCEPTION);
        mv.visitTypeInsn(NEW, Type.getInternalName(PropertyDumpException.class));
        mv.visitInsn(DUP);
        mv.visitLdcInsn("PropertyDump Error occurs While Processing ");
        mv.visitVarInsn(ALOAD, LOCAL_INDEX_EXCEPTION);
        mv.visitMethodInsn(INVOKESPECIAL, Type.getInternalName(PropertyDumpException.class), "<init>", "(Ljava/lang/String;Ljava/lang/Throwable;)V", false);
        mv.visitInsn(ATHROW);

        //never mind the numbers, cause asm will calculate it for itself
        mv.visitMaxs(4, 9);
        mv.visitEnd();
    }

    public String getSrcInternalName() {
        return srcInternalName;
    }

    public String getTargetInternalName() {
        return targetInternalName;
    }

    public String getDumpMethodName() {
        return dumpMethodName;
    }

    public String getDumpMethodDesc() {
        return dumpMethodDesc;
    }
}